#!/usr/bin/perl -w
# $Id$
use strict;
require v5.8.0;
our $VERSION = 'v1.0';

my %OPTS;
my @OPTIONS = qw/help|h|? manual|m debug test|t project|p dump|d dump-config|dc dump-data|dd check|c list|l pull reset|r no-git|ng no-svn|ns push execute|x=s/;
if(@ARGV)
{
    require Getopt::Long;
    Getopt::Long::GetOptions(\%OPTS,@OPTIONS);
}
else {
    $OPTS{help} = 1;
}


#START	//map options to actions
my $have_opts;
foreach(keys %OPTS) {
	if($OPTS{$_}) {
		$have_opts = 1;
		last;
	}
}
unless($have_opts) {
	my $first_arg = shift;
	if($first_arg and $first_arg =~ m/^(help|manual|test|project|dump|dump-config|dump-data|check|list|pull|reset|push)$/) {
		$OPTS{$first_arg} = 1;
	}
	else {
		unshift @ARGV,$first_arg;
		$OPTS{pull} = 1;
	}
}
#END	//map options to actions

if($OPTS{help}) {
    require Pod::Usage;
    Pod::Usage::pod2usage(-exitval=>0,-verbose => 1);
    exit 0;
}
elsif($OPTS{manual}) {
    require Pod::Usage;
    Pod::Usage::pod2usage(-exitval=>0,-verbose => 2);
    exit 0;
}

use Cwd qw/getcwd/;

my $F_TEST = $OPTS{test};

my %CONFIG;
$CONFIG{svn} = "https://#1.googlecode.com/svn/#2";
$CONFIG{'git:github'} = "git\@github.com:#1/#2.git";
$CONFIG{'git:gitorious'} = "git\@gitorious.org:#1/#2.git";
$CONFIG{authors} = 'authors';
$CONFIG{GS_DIR} = getcwd();
my %MACRO;

my %project;
my %sub_project;

sub run {
    print "[",getcwd(),"] ",join(" ",@_),"\n";
    return 1 if($F_TEST);
    return system(@_) == 0;
}
sub error {
    print STDERR @_;
    return undef;
}

sub parse_query {
	my $query = shift;
	if($query =~ m/^([^:]+):(.*)$/) {
		return $1,$2;
	}
	else {
		return $query;
	}
}

sub get_project_data {
	my ($name,undef) = parse_query(@_);
	return $name,($project{$name} ? $project{$name} : $sub_project{$name});
}

sub parse_project_data {
    foreach my $line (@_) {
        $_ = $line;
        chomp;
        #print STDERR "[1]",$_,"\n";
        foreach my $v_name (keys %MACRO) {
            s/#$v_name#/$MACRO{$v_name}/g;
        }
        #print STDERR "[2]",$_,"\n";
        if(m/^\s*#([^#]+)#\s*=\s*(.+?)\s*$/) {
            my $name = $1;
            my $value = $2;
            next unless($value);
            if($name =~ m/^(?:authors|user|username|email|svn|svn:.+|git|git:.+)$/) {
                $CONFIG{$name} = $value;
            }
            $MACRO{$name} = $value;
            next;
        }
		elsif(m/^\s*#/) {
			next;
		}
        my @data = (split(/\s*\|\s*/,$_),'','','','','','','');
        foreach(@data) {
            s/^\s+|\s+$//;
        }
        next unless($data[0]);
        my $name = shift @data;
        if($data[0] =~ m/.+\/.+/) {
            $sub_project{$name} = \@data;
        }
        else {
            $project{$name} = \@data;
        }
    }
}

sub translate_url {
    my $url = shift;
    my $path = shift;
	print STDERR "translate_url> $url + $path\n" if($OPTS{debug});
    if($url =~ m/#3/ and $path =~ m/^([^\/]+)\/([^\/]+)\/(.+)$/) {
        my $a = $1;
        my $b = $2;
		my $c = $3;
        $url =~ s{#1}{$a}g;
        $url =~ s{#2}{$b}g;
		$url =~ s{#3}{$c}g;
    }
    elsif($url =~ m/#2/ and $path =~ m/^([^\/]+)\/(.+)$/) {
        my $a = $1;
        my $b = $2;
        $url =~ s{#1}{$a}g;
        $url =~ s{#2}{$b}g;
    }
    else {
        $url =~ s{#1}{$path}g;
    }
	$url =~ s/#\d//g;
	$url =~ s/\/{2,}/\//g;
	$url =~ s/:\/([^\/])/:\/\/$1/;
    $url =~ s/\/+$//;
	print STDERR "translate_url>=$url\n" if($OPTS{debug});
    return $url;
}

sub push_remote {
    my @remotes;
    open FI,"-|",qw/git remote/;
    while(<FI>) {
        chomp;
        push @remotes,$_ if($_);
    }
    close FI;
    if(@remotes) {
        my $idx=0;
        my $count=@remotes;
        foreach(@remotes) {
            $idx++;
            my $url = `git config --get "remote.$_.url"`;
            chomp($url);
            print "[$idx/$count]pushing to [$_] $url ...\n";
            if(system(qw/git push/,$_,@_)!=0) {
                print "[$idx/$count]pushing to [$_] failed\n";
                return undef;
            }
        }
    }
    else {
       print "NO remotes found, stop pushing\n";
       return undef;
    }
    return 1;
}
sub get_repo {
    my ($query_name,$target,$user,@repo_data) = @_;
    my %r;
	my ($name,$new_target) = parse_query($query_name);
    $r{name} = $name;
	if($new_target) {
		$r{_target} = $target ? $target : $name;
		$r{target} = $new_target;
	}
	else {
	    $r{target} = $target ? $target : $name;
	}
    $r{user} = $user ? $user : $CONFIG{user};
    @repo_data = ("svn/$name","git/$name") unless(@repo_data);
    foreach my $url (@repo_data) {
        next unless($url);
		print STDERR "get_repo>  $url\n" if($OPTS{debug});
        if($url =~ m/^(svn|git):([^\/]+)\/(.*)$/i) {
            my $template = $CONFIG{"$1:$2"} ? $CONFIG{"$1:$2"} : "$2/$3";
			my $subname = $3;
			my $type = $1;
			if($subname and $subname =~ m/\/$/) {
				$subname = $subname . $name;
				print STDERR "get_repo> fixed name to $subname\n" if($OPTS{debug});
			}
			$url = translate_url($template,$subname ? $subname : $name);
            push @{$r{$type}},$url;
        }
        elsif($url =~ m/^(svn|git)\/(.*)$/){
            my $template = $CONFIG{$1} ? $CONFIG{$1} : "$2";
			my $subname = $2;
			my $type = $1;
			if($subname and $subname =~ m/\/$/) {
				$subname = $subname . $name;
			}
            $url = translate_url($template,$subname ? $subname : $name);
            push @{$r{$type}},$url;
        }
        elsif($url =~ m/^(svn|git):([^\/]+)$/i) {
            push @{$r{$1}},$2;
        }
        else {
            push @{$r{svn}},$url;
        }
		print STDERR "get_repo>- $url\n" if($OPTS{debug});
    }
    if($r{user} and $r{user} =~ m/(.+)\s+<([^\@]+\@[^\@]+)>\s*$/) {
        $r{username} = $1;
        $r{email} = $2;
    }
    $r{username} = $CONFIG{username} unless($r{username});
    $r{email}   = $CONFIG{email} unless($r{email});
    if($r{svn} and @{$r{svn}}) {
        my @svns = @{$r{svn}};
        $r{svn_main} = shift @svns;
        $r{svn} = \@svns;
    }
    if($r{git} and @{$r{git}}) {
        my @gits = @{$r{git}};
        $r{git_main} = shift @gits;
        $r{git} = \@gits;
    }
    return \%r;
}
sub config_repo {
	my $repo = shift;
    if($repo->{username}) {
        run(qw/git config user.name/,$repo->{username}) or return error("fatal: while git config\n");
    }
    if($repo->{email}) {
        run(qw/git config user.email/,$repo->{email}) or return error("fatal: while git config\n");
    }
    my $user_file = $CONFIG{GS_DIR} . "/" . $CONFIG{authors} . ".txt";
	if(-f $user_file) {
		run(qw/git config svn.authorsfile/,$user_file);
	}
}

sub check_repo {
    run('git','status') or return undef;
    run('git','branch','-av') or return undef;
    run('git','remote','-v') or return undef;
    return 1;
}
sub reset_repo {
	my $repo = shift;
#	system('rm',"-v",".git/config");
	system("rm","-fvr",".git/refs/remotes");
	system("rm","-fvr",".git/svn");
	config_repo($repo);
	if($repo->{svn_main} and !$OPTS{'no-svn'}) {
		unshift @{$repo->{git}},$repo->{git_main} if($repo->{git_main});
		$repo->{git_main} = undef;
		init_svn_repo('svn',$repo->{svn_main});	
	}
	if($repo->{git_main} and !$OPTS{'no-git'}) {
		unshift @{$repo->{svn}},$repo->{svn_main} if($repo->{svn_main});
		$repo->{svn_main}=undef;
		init_git_repo($repo->{git_main});		
	}
	update_repo($repo);
}
sub init_repo {
    my $repo = shift;
	config_repo($repo);

    if($repo->{svn_main} and !$OPTS{'no-svn'}) {
		unshift @{$repo->{git}},$repo->{git_main} if($repo->{git_main});
		$repo->{git_main} = undef;
        my $user_file = $CONFIG{GS_DIR} . "/" . $CONFIG{authors} . ".txt";
		$user_file = undef unless(-f $user_file);
		run(qw/git config --remove-section/,"svn-remote.svn");
		run(qw/git svn clone/, ($user_file ? ('-A',$user_file) : "--verbose"),'-s',$repo->{svn_main},".")
			or return error("fatal: while initializing $repo->{svn_main}\n");
		#init_svn_repo('svn',$repo->{svn_main},$user_file) 
			#or return error("fatal: while initializing $repo->{svn_main}\n");
    }
    if($repo->{git_main} and !$OPTS{'no-git'}) {
		unshift @{$repo->{svn}},$repo->{svn_main} if($repo->{svn_main});
		$repo->{svn_main}=undef;
		run(qw/git remote rm origin/);
		run(qw/git remote add origin/,$repo->{git_main});
		run(qw/git config branch.master.remote origin/);
		run(qw/git config branch.master.merge/,"refs/heads/master");
		run(qw/git pull/) or
			return error("fatal: while initializing $repo->{git_main}\n");
	}
    return 1;
}

sub init_svn_repo {
    my ($name,$url,$authors_file) = @_;
	run(qw/git config --remove-section/,"svn-remote." . $name);
	if($authors_file) {
		run(qw/git config svn.authorsfile/,$authors_file);
	}
    run(qw/git svn -R/,$name,qw/init -s/,$url) 
        or return error("fatal: while git svn init [$name] $url\n");
    run(qw/git svn -R/,$name,"fetch")
        or return error("fatal: while git svn fetch $name\n");
    return 1;
}

sub unique_name {
	my ($base,$pool) = @_;
	my $idx = 2;
	my $result = $base;
	while($pool->{$result}) {
		$result = $base . $idx;
		$idx++;
	}
	return $result;
}

sub init_git_repo {
	my %remotes;
	foreach my $url (@_) {
		my $name;
        if($url =~ m/^\//) {
            $name = "local";
        }
        elsif($url =~ m/^git\@([^:\/]+)[:\/](.+)$/) {
            $name = $1;
            $name =~ s/[^\.]+\.([^.]+\.[^\.]+)/$1/;
            $name =~ s/\.(?:com|org|net)$//;
        }
        elsif($url =~ m/([^\/:]+)[\/:]/) {
            $name = $1;
        }
        else {
            $name = $url;
        }
		$name = unique_name($name,\%remotes);
		$remotes{$name} = $url;
    }
	foreach my $name (keys %remotes) {
		my $url = $remotes{$name};
		run(qw/git remote rm/,$name);
		run(qw/git remote add/,$name,$url); 
	    run(qw/git fetch/,$name)  
			or error("fatal: while git fetch $name\n");
	}
    return 1;    
}
sub update_repo {
    my $repo = shift;
    if($repo->{svn} and @{$repo->{svn}} and !$OPTS{'no-svn'}) {
        foreach my $host (@{$repo->{svn}}) {
            unless(init_svn_repo(undef,$host)) {
				error("fatal: while init svn repo [$host]\n");
				next;
			}
        }
    }
    if($repo->{git} and @{$repo->{git}} and !$OPTS{'no-git'}) {
        unless(init_git_repo(@{$repo->{git}})) {
			error("fatal: while init git repo\n");
			next;
		}
    }
    run(qw/git branch -av/) or return error("fatal: while git branch\n");
    return 1;
}

sub push_repo {
	my $repo = shift;
	if($OPTS{test}) {
		push_remote('--all','--force');
	}
	else {
		push_remote('--all','--force','--dry-run');
	}
}

sub pull_repo {
	my $repo = shift;
	init_repo($repo) or
		return error("Error while initializing repo\n");
	update_repo($repo) or
		return error("Error while updating repo\n");
	return 1;
}
sub execute_repo {
	my $repo = shift;
	run($OPTS{execute});
}
my $PROGRAM_DIR = $0;
$PROGRAM_DIR =~ s/[^\/\\]+$//;
my $cwd = getcwd();
my $PROJECT_FILE;

foreach my $fn (".PROJECTS","$PROGRAM_DIR/.PROJECTS","~/git-svn/.PROJECTS") {
    if(-f $fn) {
        $PROJECT_FILE = $fn;
        last;
    }
}
if($PROJECT_FILE) {
    if(-f $PROJECT_FILE) {
        print STDERR "reading \"$PROJECT_FILE\"... ";
        open FI,"<".$PROJECT_FILE;
        parse_project_data(<FI>);
        close FI;
    }
}

if(not (@ARGV or $PROJECT_FILE)) {
    print STDERR "input projects data line by line\n";
    parse_project_data(<STDIN>);
}
if($OPTS{project}) {
	my $name = shift;
	parse_project_data(join('|',@ARGV),"\n");
	push @ARGV,$name;
}


my $total = scalar(keys %project) + scalar(keys %sub_project);
print STDERR "$total", $total > 1 ? " projects" : " project", ".\n";

#my $QUERY_NAME=shift;
#my @query = $QUERY_NAME ? ($QUERY_NAME) : (keys %project,keys %sub_project);
#my $count = $QUERY_NAME ? 1 : $total;
my @query = @ARGV ? @ARGV : (keys %project,keys %sub_project);


if($OPTS{'list'}) {
	print STDERR join("\n",sort(@query)),"\n";
	exit 0;
}

if($OPTS{'dump'}) {
    $OPTS{'dump-config'} = 1;
    $OPTS{'dump-data'} = 1;
}

if($OPTS{'dump-config'}) {
    use Data::Dumper;
    print Data::Dumper->Dump([\%CONFIG],["*CONFIG"]);
}

if($OPTS{'dump-data'}) {
    use Data::Dumper;
#    my @query = $QUERY_NAME ? ($QUERY_NAME) : (keys %project,keys %sub_project);
    foreach my $query_text (@query) {
        my ($name,$pdata) = get_project_data($query_text);
        my $repo = get_repo($query_text,@{$pdata});
        print Data::Dumper->Dump([$repo],["*$name"]);
    }
}
if($OPTS{'dump-config'} or $OPTS{'dump-data'}) {
    exit 0;
}

my $idx = 0;
my $count = scalar(@query);

foreach my $action (qw/execute check reset push pull update/) {
	if($OPTS{$action}) {
		print STDERR "ready to $action $count ", $count > 1 ? "projects" : "project", " ...\n";
	    foreach my $query_text (@query) {
	        chdir($cwd) or die("$!\n");
	        $idx++;
			my ($name,$pdata) = get_project_data($query_text);
	        print STDERR "[$idx/$count] project $name...\n";
	    	if((!$pdata) or (!ref $pdata)) {
	    		print STDERR "[$idx/$count] project $name not defined.\n";
	    		next;
	    	}
	        my $repo = get_repo($query_text,@{$pdata});
			print STDERR "[$idx/$count] $name -> $repo->{target}\n";
			print STDERR "[$idx/$count] changing to target directory\n";
	        unless(-d $repo->{target}) {
				if($action eq 'pull') {
					if(!run(qw/git init/,$repo->{target}) ) {
						error("[$idx/$count] init $repo->{target} failed!\n\n");
						next;
					}
				}
				else {
		            error("[$idx/$count] target not exists: $repo->{target}\n\n");
		            next;
				}					
	        }
	        unless(chdir($repo->{target})) {
	            error("[$idx/$count] error while chdir to $repo->{target}\n\n");
	            next;
	        }
	        unless(eval($action . "_repo(\$repo)")) {
	            error("[$idx/$count] error while to $action $name\n\n");
	            next;
	        }
	        print STDERR "\n";
		}
		exit 0;
	}		
}

print STDERR "Unknown action or no action specified\n";
exit 1;

__END__

=pod

=head1  NAME

gsbridge - git-svn projects manager

=head1  SYNOPSIS

gsbridge [options] [action] [project_name|project_name:target]...
	gsbridge --pull firefox
	gsbridge pull firefox
	gsbirdge pull firefox:test_firefox
	gsbridge firefox:test_firefox

=head1  OPTIONS

=over 12

=item B<-ng>,B<--no-git>

Disable git repositories

=item B<-ns>,B<--no-svn>

Disable subversion repositories

=item B<-r>,B<--reset>

Re-configure projects

=item B<-c>,B<--check>

Check projects status

=item B<-p>,B<--project>

Target and define project from command line

=item B<-t>,B<--test>

Testing mode

=item B<--dump>

Dump CONFIG and DATA

=item B<--dump-config>

Dump CONFIG

=item B<--dump-data>

Dump DATA

=item B<-l>,B<--list>

List projects

=item B<-h>,B<--help>

Print a brief help message and exits.

=item B<--manual>,B<--man>

View application manual

=back

=head1  FILES

=item B<./.PROJECTS>

Default projects definition file, one line a project, 
echo field separated by |.

=back

=head1 PROJECTS FILE FORMAT
#MACRO1#=....
#MACRO2#=....
name	|[target]	|[user]	|repo1	|repo2	|repo3...

=head1  DESCRIPTION

git-svn projects manager

=head1  CHANGELOG

    2010-11-01  xiaoranzzz  <xiaoranzzz@myplace.hell>
        
        * file created.
	
	2010-11-25	xiaoranzzz	<xiaoranzzz@myplace.hell>
		
		* updated projects definition format
		* added two actions: checking and resetting
		* version 1.0

=head1  AUTHOR

xiaoranzzz <xiaoranzzz@myplace.hell>

=cut

#       vim:filetype=perl
